/* 
 * StikiWeb Wiki - a Wiki for Hierarchical Organizations
 * $Id: StikiWebTranslator.java,v 1.12 2005/01/21 05:43:23 biglee Exp $
 * 
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is the StikiWeb Wiki 
 *
 * The Initial Developer of the Original Code is Lee Haslup (biglee@haslups.com).
 * Portions created by the Initial Developer are Copyright (C) 2004
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * ***** END LICENSE BLOCK ***** 
 */
package org.stikiweb.translations;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.Vector;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.stikiweb.contentMagnager.AttachmentManager;
import org.stikiweb.contentMagnager.PageInfoNode;
import org.stikiweb.contentMagnager.PageManager;
import org.stikiweb.contentMagnager.PageManagerException;
import org.stikiweb.contentMagnager.PermissionManager;

/**
 * @author Allen L (BigLee) Haslup
 *
 */
public class StikiWebTranslator {

	protected class TranslatorState {

		// stack values representing html tags.
		public static final int NO_TAG = 0;
		public static final int P_TAG = 1;
		public static final int TABLE_TAG = 2;
		public static final int TABLE_END = -TABLE_TAG;
		public static final int TR_TAG = 3;
		public static final int TD_TAG = 4;
		public static final int TH_TAG = 5;
		public static final int B_TAG = 6;
		public static final int I_TAG = 7;
		public static final int OL_TAG = 8;
		public static final int UL_TAG = 9;
		public static final int FONT_TAG = 10;
		public static final int H1_TAG = 11;
		public static final int H2_TAG = 12;
		public static final int H3_TAG = 13;
		public static final int PRE_TAG = 14;
		public static final int HR_TAG=15;

		// stack of enclosing tags and attributes
		// (so we can keep things well formed)
		protected Stack tagStack = new Stack();
		protected Stack attrStack = new Stack();

		// keep track of how many tables we are nested so
		// we can be sure we close as many as we open.
		public int tableNestingLevel = 0;

		// keep track of which emphasis attributes are on
		private boolean inBold = false;
		private boolean inItalics = false;
		private boolean inFont = false;
		private boolean inPara = false;

		// Remember if we are in an autotable.
		private boolean inAutoTable = false;

		public void push(int tag, String attributes) {
			tagStack.push(new Integer(tag));
			attrStack.push(attributes);
			switch (tag) {
				case TABLE_TAG :
					tableNestingLevel++;
					break;
				case B_TAG :
					inBold = true;
					break;
				case I_TAG :
					inItalics = true;
					break;
				case FONT_TAG :
					inFont = true;
					break;
			}
		}

		public void pop() {
			if (!tagStack.isEmpty()) {
				int poppedTag = ((Integer) tagStack.pop()).intValue();
				attrStack.pop();
				switch (poppedTag) {
					case TABLE_TAG :
						tableNestingLevel--;
						break;
					case B_TAG :
						inBold = false;
						break;
					case I_TAG :
						inItalics = false;
						break;
					case FONT_TAG :
						inFont = false;
				}
			}
		}

		public int peekTag() {
			if (tagStack.isEmpty()) {
				return -1;
			}
			return ((Integer) tagStack.peek()).intValue();
		}

		public String peekAttr() {
			if (attrStack.isEmpty()) {
				return "";
			}
			return (String) attrStack.peek();
		}
		/**
		 * @return
		 */
		public boolean isInBold() {
			return inBold;
		}

		/**
		 * @return
		 */
		public boolean isInFont() {
			return inFont;
		}

		/**
		 * @return
		 */
		public boolean isInItalics() {
			return inItalics;
		}

		/**
		 * @return
		 */
		public boolean isInPara() {
			return inPara;
		}

		/**
		 * @return
		 */
		public int getTableNestingLevel() {
			return tableNestingLevel;
		}

		public int stackSize() {
			return tagStack.size();
		}

		/**
		 * @return
		 */
		public boolean isInAutoTable() {
			return inAutoTable;
		}

		/**
		 * @param b
		 */
		public void setInAutoTable(boolean b) {
			inAutoTable = b;
		}

	}
	
	public String translateText(String rawText, String currentPage, String nestedPage, HttpServletRequest req)
		throws IOException {
		StringBuffer htmlTextBuffer = new StringBuffer();
		StringReader rawTextReader = new StringReader(rawText);
		BufferedReader rawBR = new BufferedReader(rawTextReader);
		TranslatorState state = new TranslatorState();
		String rawLine = rawBR.readLine();
		while (rawLine != null) {
			htmlTextBuffer.append(translateLine(rawLine, currentPage, nestedPage, state, req, false));
			rawLine = rawBR.readLine();
		}
		// make sure we close all tags even if the page doesn't
		// tell us to.  
		int tag = state.peekTag();
		while (tag > 0) {
			htmlTextBuffer.append(closeTagFor(tag));
			state.pop();
			tag = state.peekTag();
		}
		return htmlTextBuffer.toString();
	}

	protected String openTagFor(int tag, String atts) {
		switch (tag) {
			case TranslatorState.P_TAG :
				return "<p " + atts + " >\n";
			case TranslatorState.TABLE_TAG :
				return "<table " + atts + " >\n";
			case TranslatorState.TR_TAG :
				return "<tr " + atts + " >\n";
			case TranslatorState.TD_TAG :
				return "<td " + atts + " >\n";
			case TranslatorState.TH_TAG :
				return "<th " + atts + " >\n";
			case TranslatorState.B_TAG :
				return "<b " + atts + " >\n";
			case TranslatorState.I_TAG :
				return "<i " + atts + " >\n";
			case TranslatorState.OL_TAG :
				return "<ol " + atts + " >\n";
			case TranslatorState.UL_TAG :
				return "<ul " + atts + " >\n";
			case TranslatorState.FONT_TAG :
				return "<font " + atts + " >\n";
			case TranslatorState.H1_TAG :
				return "<h1 " + atts + " >\n";
			case TranslatorState.H2_TAG :
				return "<h2 " + atts + " >\n";
			case TranslatorState.H3_TAG :
				return "<h3 " + atts + " >\n";
			case TranslatorState.PRE_TAG :
				return "<pre " + atts + " >\n";
		}
		return "";
	}

	protected String closeTagFor(int tag) {
		switch (tag) {
			case TranslatorState.P_TAG :
				return "</p>\n";
			case TranslatorState.TABLE_TAG :
				return "</table>\n";
			case TranslatorState.TR_TAG :
				return "</tr>\n";
			case TranslatorState.TD_TAG :
				return "</td>\n";
			case TranslatorState.TH_TAG :
				return "</th>\n";
			case TranslatorState.B_TAG :
				return "</b>\n";
			case TranslatorState.I_TAG :
				return "</i>\n";
			case TranslatorState.OL_TAG :
				return "</ol>\n";
			case TranslatorState.UL_TAG :
				return "</ul>\n";
			case TranslatorState.FONT_TAG :
				return "</font>\n";
			case TranslatorState.H1_TAG :
				return "</h1>\n";
			case TranslatorState.H2_TAG :
				return "</h2>\n";
			case TranslatorState.H3_TAG :
				return "</h3>\n";
			case TranslatorState.PRE_TAG :
				return "</pre>\n";
		}
		return "";
	}

	protected String translateLine(
		String rawLine,
		String currentPage,
		String nestedPage,
		TranslatorState state,
		HttpServletRequest req,
		boolean recursed) {

		// Special Line-level transformations...
		// {| blah blah = <table blah blah>
		// |- = <tr> or </td></tr><tr> or </th></tr><tr>

		// ==============================
		// decide what sort of line we are seeing
		// ==============================

		// comment line -- shortcut the process
		if (rawLine.startsWith("~~>")) {
			return "";
		}

		// default
		int tagThisLine = TranslatorState.NO_TAG;

		// blank lines end paragraphs (<p> tag)
		if (rawLine.trim().length() == 0) {
			tagThisLine = TranslatorState.P_TAG;
		}
		// Lines that start with blanks are not translated (<pre>)
		else if (rawLine.startsWith(" ")) {
			tagThisLine = TranslatorState.PRE_TAG;
		}
		// Lines that start with !!! are major headings
		else if (rawLine.startsWith("!!!")) {
			tagThisLine = TranslatorState.H1_TAG;
		}
		// !! headings
		else if (rawLine.startsWith("!!")) {
			tagThisLine = TranslatorState.H2_TAG;
		}
		// ! minor headings
		else if (rawLine.startsWith("!")) {
			tagThisLine = TranslatorState.H3_TAG;
		}
		// HR Tag
		else if (rawLine.startsWith("---")) {
			tagThisLine = TranslatorState.HR_TAG;
		}
		// {| [params] = <table [params] >
		else if (rawLine.startsWith("{|")) {
			tagThisLine = TranslatorState.TABLE_TAG;
		}
		// |} = </table>
		else if (rawLine.startsWith("|}")) {
			tagThisLine = TranslatorState.TABLE_END;
		}
		// |- = <tr>
		else if (rawLine.startsWith("|-")) {
			tagThisLine = TranslatorState.TR_TAG;
		}
		// || [params] | = <td [params]>
		else if (rawLine.startsWith("||")) {
			tagThisLine = TranslatorState.TD_TAG;
		}
		// |! [params] | = <th [params]>
		else if (rawLine.startsWith("|!")) {
			tagThisLine = TranslatorState.TH_TAG;
		}
		String retval = "";

		// ========================================
		// generate any tags that should come before this one
		// ========================================
		// some tags introduce a sequence that should be 
		// completed before we start doing other things.

		// if we are not in a table and we see a row or a cell tag
		// go ahead and open an autotable.
		if (state.tableNestingLevel == 0
			&& (tagThisLine == TranslatorState.TR_TAG
				|| tagThisLine == TranslatorState.TH_TAG
				|| tagThisLine == TranslatorState.TD_TAG)) {
			retval += "<table  border>\n";
			state.push(TranslatorState.TABLE_TAG, "border");
			state.setInAutoTable(true);
		}

		// autotables can only contain row and column tag lines.
		// if we see anything else, we close the table.
		if (state.tableNestingLevel == 1
			&& state.inAutoTable
			&& tagThisLine != TranslatorState.TR_TAG
			&& tagThisLine != TranslatorState.TH_TAG
			&& tagThisLine != TranslatorState.TD_TAG
			&& tagThisLine != TranslatorState.TABLE_END) {
			while (state.peekTag() != TranslatorState.TABLE_TAG) {
				int tag = state.peekTag();
				retval += closeTagFor(tag);
				state.pop();
			}
			retval += "</table>\n";
			state.pop();
			state.setInAutoTable(false);
		}

		// if we are in an autotable and this is a follow-on line
		// force a row since autotables produce a row for each
		// input line.  The "recursed" variable allows us to 
		// ignore recursive calls from the TD and TH tag
		// translation sections below.  Those do not represent
		// follow-on lines, they are calling this routine recursively
		// to process the tail-end of a table cell line.
		if (state.inAutoTable
			&& state.peekTag() != TranslatorState.TABLE_TAG
			&& tagThisLine != TranslatorState.TR_TAG
			&& tagThisLine != TranslatorState.TABLE_END
			&& state.tableNestingLevel == 1
			&& !recursed) {
			while (state.peekTag() != TranslatorState.TABLE_TAG) {
				retval += closeTagFor(state.peekTag());
				state.pop();
			}
			retval += "<tr>\n";
			state.push(TranslatorState.TR_TAG, "");
		}

		// if we just pushed a table and this line does not represent a row
		// then we provide the row tag with no parameters.
		if (state.peekTag() == TranslatorState.TABLE_TAG
			&& tagThisLine != TranslatorState.TR_TAG) {
			state.push(TranslatorState.TR_TAG, "");
			retval += "<tr>\n";
		}

		// if we are in a new row and this line does not define either a TD or TH cell
		// then we provide a TD cell
		if (state.peekTag() == TranslatorState.TR_TAG
			&& tagThisLine != TranslatorState.TD_TAG
			&& tagThisLine != TranslatorState.TH_TAG) {
			state.push(TranslatorState.TD_TAG, "");
			retval += "<td>\n";
		}

		// if we are in a PRE block and this line is not PRE then end the block
		if (state.peekTag() == TranslatorState.PRE_TAG
			&& tagThisLine != TranslatorState.PRE_TAG) {
			state.pop();
			retval += "</pre>\n";
		}

		// if this line does not represent a tag then it should be in a paragraph
		// (shooting for xhtml here but will it work??)
		if (tagThisLine == TranslatorState.NO_TAG
			&& state.peekTag() != TranslatorState.P_TAG) {
			state.push(TranslatorState.P_TAG, "");
			retval += "<p>\n";
		}

		// likewise, if this line is a tag and we are in a paragraph we need to end it.
		if (tagThisLine != TranslatorState.NO_TAG
			&& state.peekTag() == TranslatorState.P_TAG) {
			state.pop();
			retval += "</p>\n";
		}

		// if we are starting a new cell or row or ending a table
		// then we need to end any TD or TH
		if (tagThisLine == TranslatorState.TD_TAG
			|| tagThisLine == TranslatorState.TH_TAG
			|| tagThisLine == TranslatorState.TR_TAG
			|| tagThisLine == TranslatorState.TABLE_END) {
			int curtag = state.peekTag();
			if (curtag == TranslatorState.TH_TAG) {
				state.pop();
				retval += "</th>\n";
			} else if (curtag == TranslatorState.TD_TAG) {
				state.pop();
				retval += "</td>\n";
			}
		}

		// if we are ending a table and we are still in a row
		// tag we need to end it.
		if (tagThisLine == TranslatorState.TABLE_END
			&& state.peekTag() == TranslatorState.TR_TAG) {
			state.pop();
			retval += "</tr>\n";
		}
		
		// if we see a row tag and we are are still in a previous row
		// end the previous row
		if(tagThisLine == TranslatorState.TR_TAG
			&& state.peekTag() == TranslatorState.TR_TAG) {
				state.pop();
				retval += "</tr>\n";
			}

		// ==================
		// Translate the tag itself.
		// ==================

		String atts;
		switch (tagThisLine) {

			// if this line doesn't map to any special HTML
			// transformation then process it as a run of text.
			case TranslatorState.NO_TAG :
				retval += translateRun(rawLine, currentPage, nestedPage, state, req);
				break;

				// paragraph tag 
			case TranslatorState.P_TAG :
				// if already in paragraph close and reopen
				// (no net stack action)
				if(state.peekTag()==TranslatorState.P_TAG) {
					retval += "</p><p>\n";				
				}
				// else open a new paragraph and push it
				else {
					retval += "<p>\n";
					state.push(TranslatorState.P_TAG,"");
				}
				break;

				// table tag - everything after {| is an attribute of the table
			case TranslatorState.TABLE_TAG :
				atts = sanitizeParms(rawLine.substring(2).trim());
				retval += "<table " + atts + " >\n";
				state.push(TranslatorState.TABLE_TAG, atts);
				break;

				// table end tag
			case TranslatorState.TABLE_END :
				if (state.tableNestingLevel > 0) {
					retval += "</table>\n";
					state.pop();
				}
				break;

				// row tag - everything after |-[-*] is an attribute of the row.
			case TranslatorState.TR_TAG :
				// the initial run of dashes are not part of the params.
				int len = rawLine.length();
				int rover = 1;
				while (rover < len && rawLine.charAt(rover) == '-') {
					rover++;
				}
				atts = sanitizeParms(rawLine.substring(rover).trim());
				retval += "<tr " + atts + " >\n";
				state.push(TranslatorState.TR_TAG, atts);
				break;

				// TD or TH tag everything btw || and | or |! and | is an attribute
				// anything after that | up to (but not including) the next unescaped |
				// is a data run.  That unescaped | starts, in effect, a new line.
			case TranslatorState.TD_TAG :
			case TranslatorState.TH_TAG :

				int parmEnd = findUnescaped('|', rawLine, 2);
				if (parmEnd < 0) {
					parmEnd = 2;
				}
				atts = sanitizeParms(rawLine.substring(2, parmEnd));
				if (tagThisLine == TranslatorState.TD_TAG) {
					retval += "<td " + atts + ">\n";
					state.push(TranslatorState.TD_TAG, atts);
				} else {
					retval += "<th " + atts + ">\n";
					state.push(TranslatorState.TH_TAG, atts);
				}
				int runEnd = findUnescaped('|', rawLine, parmEnd + 1);
				// has to be "||" or "|!" to avoid problem with links
				while(runEnd>0 && runEnd<rawLine.length()-1&&"|!".indexOf(rawLine.charAt(runEnd+1))<0) {
					runEnd = findUnescaped('|',rawLine,runEnd + 1);
				}
					
				if (runEnd < 0) {
					runEnd = rawLine.length();
				}
				String run = rawLine.substring(parmEnd + 1, runEnd).trim();
				if (run.length() > 0) {
					retval += "<p>\n";
					state.push(TranslatorState.P_TAG, "");
					retval += translateRun(run, currentPage,nestedPage, state, req);
					retval += "</p>\n";
					state.pop();
				}
				if (runEnd < rawLine.length()) {
					retval
						+= translateLine(
							rawLine.substring(runEnd),
							currentPage,
							nestedPage,
							state,
							req,
							true);
				}
				break;

			case TranslatorState.PRE_TAG :
				if (state.peekTag() != TranslatorState.PRE_TAG) {
					retval += "<pre>\n";
					state.push(TranslatorState.PRE_TAG, "");
				}
				retval += recodePrecode(rawLine.substring(1)) + "\n";
				break;

			case TranslatorState.H1_TAG :
				retval += "<h1>\n";
				state.push(TranslatorState.H1_TAG, "");
				retval += translateRun(rawLine.substring(3), currentPage, nestedPage, state, req);
				retval += "</h1>\n";
				state.pop();
				break;

			case TranslatorState.H2_TAG :
				retval += "<h2>\n";
				state.push(TranslatorState.H2_TAG, "");
				retval += translateRun(rawLine.substring(2), currentPage, nestedPage, state, req);
				retval += "</h2>\n";
				state.pop();
				break;

			case TranslatorState.H3_TAG :
				retval += "<h3>\n";
				state.push(TranslatorState.H3_TAG, "");
				retval += translateRun(rawLine.substring(1), currentPage, nestedPage, state, req);
				retval += "</h1>\n";
				state.pop();
				break;
				
			case TranslatorState.HR_TAG:
				retval += "<hr />\n";
				// the rest of the line starting w/ --- is ignored.
				break;
		}

		return retval;
	}

	protected String recodePrecode(String in) {
		StringBuffer sb = new StringBuffer();
		int len = in.length();
		for (int i = 0; i < len; i++) {
			char c = in.charAt(i);
			switch (c) {
				case '<' :
					sb.append("&lt;");
					break;
				case '>' :
					sb.append("&gt;");
					break;
				case '&' :
					sb.append("&amp;");
					break;
				default :
					sb.append(c);
			}
		}
		return sb.toString();
	}

	// some useful constants for running text translation
	protected static final int ESCSCORE_2 = '\\' * 1000 + '_';
	protected static final int BOLD_2 = '_' * 1000 + '_';
	protected static final int ESCQUOTE_2 = '\\' * 1000 + '\'';
	protected static final int ITALIC_2 = '\'' * 1000 + '\'';
	protected static final int ESCBRAK_2 = '[' * 1000 + '[';
	protected static final int ESCBRAK_2B = '\\' * 1000 + '[';
	protected static final int BRAK_1 = '[';
	protected static final int ESCSLASH_2 = '\\' * 1000 + '\\';
	protected static final int SPACE_2 = '\\' * 1000 + 'b';
	protected static final int SPACES_2 = '\\' * 1000 + 's';
	protected static final int ESCBRACE_2 = '\\' * 1000 + '{';
	protected static final int FORCELINE_2 = '\\' * 1000 + 'n';
	protected static final int PLUG_2 = '{' * 1000 + '{';
	protected static final int WICKIT_1 = '<';
	protected static final int AMP_1 = '&';
	protected static final int NIL = 0;

	protected String translateRun(
		String raw,
		String currentPage,
		String nestedPage,
		TranslatorState state,
		HttpServletRequest req) {
		// if there is nothing to translate, just return
		int len = raw.length();
		if (len == 0)
			return "";

		// remember the stack size so we can be sure to pop
		// off any markup that the user didn't close out.
		int initSize = state.stackSize();

		// some setup.  The use of these vars is described below.
		int rover = 0;
		int c1 = raw.charAt(0);
		int c2 = (len == 1) ? 0 : raw.charAt(1);
		int c12 = c1 * 1000 + c2;
		StringBuffer sb = new StringBuffer();

		// this loop pulls off characters two at a time from the raw 
		// string.  "rover" is our current location in the string.  "c1"
		// is the char at that location unless that location is beyond
		// the end of string, in which case it is NIL.  'c2' is the next 
		// char (again, or NIL).  "c12" is an encodeing of c1 ans
		// c2.  The code inside the loop attempts to deal with any
		// markup starting at rover and sets "roll" to the number of
		// characters it has converted.  At the bottom of the loop
		// rover is advanced by the number of chars indicated by
		// roll and c1, c2 and c12 are updated.  The loop ends
		// when c1 is NIL indicating that rover is beyond the end
		// of the string.
		while (c1 != NIL) {

			int roll = 0;

			// first deal with two character codes
			switch (c12) {

				case BOLD_2 :
					// messy bit here...
					// before we can emit the end tag for bold we need
					// to end any tags pushed on top of it.  Then we can
					// emit the end bold tag and re-emit all the other tags 
					// that were in effect before we started.
					// Note the assumption here that nothing very 
					// significant is ever pushed on top of a bold tag.
					// if that happens then this code will break in
					// many amusing ways...
					if (state.isInBold()) {
						Stack stag = new Stack();
						Stack sapp = new Stack();
						while (state.peekTag() != TranslatorState.B_TAG) {
							stag.push(new Integer(state.peekTag()));
							sapp.push(state.peekAttr());
							sb.append(closeTagFor(state.peekTag()));
							state.pop();
						}
						sb.append("</b>");
						state.pop();
						while (stag.size() > 0) {
							int restoreTag = ((Integer) stag.pop()).intValue();
							String restoreAtts = (String) sapp.pop();
							sb.append(openTagFor(restoreTag, restoreAtts));
							state.push(restoreTag, restoreAtts);
						}
					} else {
						sb.append("<b>");
						state.push(TranslatorState.B_TAG, "");
					}
					roll = 2;
					break;

				case ITALIC_2 :
					// See discussion of bold tag above.
					if (state.isInItalics()) {
						Stack stag = new Stack();
						Stack sapp = new Stack();
						while (state.peekTag() != TranslatorState.I_TAG) {
							stag.push(new Integer(state.peekTag()));
							sapp.push(state.peekAttr());
							sb.append(closeTagFor(state.peekTag()));
							state.pop();
						}
						sb.append("</i>");
						state.pop();
						while (stag.size() > 0) {
							int restoreTag = ((Integer) stag.pop()).intValue();
							String restoreAtts = (String) sapp.pop();
							sb.append(openTagFor(restoreTag, restoreAtts));
							state.push(restoreTag, restoreAtts);
						}
					} else {
						sb.append("<i>");
						state.push(TranslatorState.I_TAG, "");
					}
					roll = 2;
					break;

				case ESCBRACE_2 :
				case ESCBRAK_2 :
				case ESCBRAK_2B :
				case ESCQUOTE_2 :
				case ESCSCORE_2 :
				case ESCSLASH_2 :
					// escaped characters.  emit the second one.
					// ie. when we see \{ we emit the {
					sb.append((char)c2);
					roll = 2;
					break;
					
				case FORCELINE_2:
					// force a newline
					sb.append("<br />");
					roll = 2;
					break;

				case SPACE_2 :
					// non-breaking space
					sb.append("&nbsp;");
					roll = 2;
					break;

				case SPACES_2 :
					// a run of non-breaking spaces.
					int runsize = 0;
					int rover2 = rover + 2;
					roll = 2;
					while (rover2 < len
						&& Character.isDigit(raw.charAt(rover2))) {
						runsize = runsize * 10 + raw.charAt(rover2) - '0';
						rover2++;
						roll++;
					}
					// arbitrary upper bound on runsize.
					if (runsize > 200) {
						runsize = 200;
					}
					for (int i = 0; i < runsize; i++) {
						sb.append("&nbsp;");
					}
					break;
			}

			// if we didn't recognize a two character code do
			// a single character.
			if (roll == 0) {

				// so we don't hang if we forget to set it somewhere...
				roll = 1;

				switch (c1) {
					case AMP_1 :
						sb.append("&amp;");
						roll = 1;
						break;

					case WICKIT_1 :
						sb.append("&lt;");
						roll = 1;
						break;

					case BRAK_1 :
						int rover2 = rover + 1;
						while (rover2 < len && raw.charAt(rover2) != ']') {
							rover2++;
						}
						if (rover2 < len) {
							sb.append(
								translateLinkBody(
									raw.substring(rover + 1, rover2),
									currentPage, 
									nestedPage,
									req));
							roll = rover2 + 1 - rover;
						} else {
							sb.append("[");
							roll = 1;
						}
						break;

					default :
						sb.append((char) c1);
						roll = 1;
						break;
				}
			}
			rover += roll;
			c1 = (rover < len) ? raw.charAt(rover) : NIL;
			c2 = (rover + 1 < len) ? raw.charAt(rover + 1) : NIL;
			c12 = c1 * 1000 + c2;
		}

		// close out any tags we opened.
		while (state.stackSize() > initSize) {
			int tag = state.peekTag();
			sb.append(closeTagFor(tag));
			state.pop();
		}
		return sb.toString() + "\n";
	}

	protected String translateLinkBody(String body, String currentPage, String nestedPage, HttpServletRequest req) {
		String text, link;
		PermissionManager pm = PermissionManager.solo();
		String user = pm.getUserName(req);
		HttpSession session = req.getSession();
		HashMap vars = (HashMap)session.getAttribute("vars");
		if(vars==null) {
			vars = new HashMap();
			session.setAttribute("vars",vars);
		}

		// split the contents of the link body into the link and the link text.
		// [ linktext ] : link=text="linktext" and textIsLink=true
		// [ mytext | alink ] : text=mytext, link = alink and textIsLink=false
		int len = body.length();
		int split = body.indexOf('|');
		boolean linkIsText = false;
		if (split >= 0) {
			text = body.substring(0, split).trim();
			link = body.substring(split + 1, len).trim();
		} else {
			text = body.trim();
			link = text;
			linkIsText = true;
		}
		
		// handle link prefix characters
		boolean tentative = false;
		boolean inline = false;
		boolean relative = false;
		String popper = "";
		String sider = "";
		while(link.length()>0 && "?@^_<>".indexOf(link.charAt(0))>=0) {
			
			switch (link.charAt(0)) {
			// if the link to the page name is preceeded by a question
			// mark then the link is tentative.  A tentative link disappears
			// if the user does not have access to the referenced page.
			case '?':
				// mark the link tentative.
				tentative = true;
				break;
				
			case '_':
				// mark the link relative
				relative=true;
				break;
			
			// if the link to the page name is preceeded by a carat (^)
			// then the link is a popup.  
			case '^':
				// mark the link as a popup. (specify link target attribute)
				popper=" target='_blank'";
				break;
			
			// if the link is preceeded by an at sign (@) then we will attempt to
			// inline the information referred to.  If it is a recognized image type
			// we will generate an IMG tag.  If it is a wiki page we will include it.
			// Otherwise we will ignore the at sign and process it normally.
			case '@':
				// mark the link as inline.
				inline = true;
				break;
			case '<':
				// mark the inline link as left aligned.
				sider=" align='left'";
				break;
			case '>':
				// mark the inline link as right aligned.
				sider=" align='right'";
				break;
			}
			
			// having processed the prefix character, remove it from the link
			link = link.substring(1).trim();
			if (linkIsText) {
				text = text.substring(1).trim();
			}
			
		}
		
		// text postfix string
		// if the link text ends with \n then a line break will follow
		// the link if the link is not suppressed for NO_ACCESS
		String liner = "";
		if (text.endsWith("\\n")) {
			liner = "<br />";
			text = text.substring(0, text.length() - 2);
			if (linkIsText) {
				link = link.substring(0, link.length() - 2);
			}
		}
		
		// if the link is followed by a list of params in parentheses
		// parse them into the linkparms hashmap
		int parenloc = link.indexOf('(');
		HashMap linkparms = null;
		if(parenloc>0) {
			linkparms = parseLinkParms(link);
			link=link.substring(0,parenloc).trim();
			if(linkIsText) {
				text=link;
			}
		}
		
		// if the link starts with "var?" then it is a redirect through
		// a variable.  for instance, [ var?foo ] is a link to whatever
		// page name is stored in the variable "foo".  The variable
		// name can be followed by a default link.   [var?foo,bar]
		// is a link to the contents of foo if foo is defined and a link
		// to bar if foo is not defined.
		if(link.toLowerCase().startsWith("var?")) {
			String varn = link.substring(4).trim();
			int varsplit = varn.indexOf(',');
			String deftLink = null;
			if(varsplit>0) {
				deftLink = varn.substring(varsplit+1);
				varn = varn.substring(0,varsplit);
			}
			String newLink = (String)vars.get(varn);
			if(newLink!=null && newLink.length()>0) {
				link = newLink;
				if(link.charAt(0)=='_') {
					link=link.substring(1).trim();
					relative = true;
				}
				if(linkIsText) {
					text=link;
				}
			}
			else if(deftLink!=null) {
				link=deftLink;
				if(link.charAt(0)=='_') {
					link=link.substring(1).trim();
					relative = true;
				}
				if(linkIsText) {
					text=link;
				}
			}
		}
		
		// if the link is relative prepend the nested page
		if(relative) {
			String dot = link.startsWith("/")?"":".";
			link = nestedPage+dot+capitalize(link);
			// Judgement call here... my sense is that users prob. do not
			// want nested page name prepended.
			if(linkIsText) {
				text = capitalize(text);
			}
		}

		// if the link is empty then we just return the input text
		// w/o translation.
		if (link.length() == 0) {
			return "[" + body + "]";
		}

		// text prefix string
		// if the text starts with a star then it is an image source for 
		// an input type=image control.
		boolean textIsImage = false;
		if(!linkIsText && text.startsWith("*")) {
			textIsImage=true;
			text=text.substring(1).trim();
			// if image is a page attachment (no "http:" but with a slash)
			if(text.indexOf(':')<0 && text.indexOf('/')>=0) {
				text = "viewAttachment.do?name="+convertName(text);
			}
		}

		// gotta have some text for the user to click on
		if (text.length() == 0) {
			text = "(link)";
		}
		
		// Convert 'dangerous' characters such as < and &
		text = sanitizeText(text);
		
		// plugins are links starting with a dollar sign
		if(link.charAt(0)=='$') {
			String sayNoIcon="";
			if(link.equalsIgnoreCase("$login")){
				if(tentative && !"Visitor".equalsIgnoreCase(pm.getUserName(req))) {
					return "";
				}
				if(linkIsText) {
					text="Login";
				}
				if(textIsImage) {
					text = "<img src='"+text+"' border='0' />";
				}
				return "<a href='http:login.do'"
					+ popper
					+ ">"
					+ text
					+ "</a>"
					+ liner;
			}
			else if(link.equalsIgnoreCase("$logout")){
				if(tentative && "Visitor".equalsIgnoreCase(pm.getUserName(req))) {
					return "";
				}
				if(linkIsText) {
					text="Logout";
				}
				if(textIsImage) {
					text = "<img src='"+text+"' border='0' />";
				}
				return "<a href='http:logout.do'"
					+ popper
					+ ">"
					+ text
					+ "</a>"
					+ liner;
			}
			else if(link.equalsIgnoreCase("$edit")){
				if(currentPage.length()==0
				   || pm.userPermLevelForPage(pm.getUserName(req),currentPage)<PermissionManager.EDIT) {
					if(tentative) {
						return "";
					}
					sayNoIcon="<img src='images/stop.png' border='0' />";
				}
				if(linkIsText) {
					text="Edit page";
				}
				if(textIsImage) {
					text = "<img src='"+text+"' border='0' />";
				}
				return "<a href='http:editPage.do?name="
					+ currentPage
					+ "&operation=EDIT'"
					+ popper
					+ " >"
					+ text
					+ sayNoIcon
					+ "</a>"
				+ liner;
			}
			else if(link.equalsIgnoreCase("$audit")){
				if(currentPage.length()==0
				   || pm.userPermLevelForPage(pm.getUserName(req),currentPage)<PermissionManager.AUDIT) {
					if(tentative) {
						return "";
					}
					sayNoIcon="<img src='images/stop.png' border='0' />";
				}
				if(linkIsText) {
					text="View page source";
				}
				if(textIsImage) {
					text = "<img src='"+text+"' border='0' />";
				}
				return "<a href='http:editPage.do?name="
					+ currentPage
					+ "&operation=AUDIT'"
					+ popper
					+ " >"
					+ text
					+ sayNoIcon
					+ "</a>"
				+ liner;
			}
			 else if(link.equalsIgnoreCase("$comment")){
				if(currentPage.length()==0
				   || pm.userPermLevelForPage(pm.getUserName(req),currentPage)<PermissionManager.COMMENT) {
					if(tentative) {
						return "";
					}
					sayNoIcon="<img src='images/stop.png' border='0' />";
				}
				if(linkIsText) {
					text="Leave comment";
				}
				if(textIsImage) {
					text = "<img src='"+text+"' border='0' />";
				}
				return "<a href='http:editPage.do?name="
					+ currentPage
					+ "&operation=COMMENT'"
					+ popper
					+ " >"
					+ text
					+ sayNoIcon
					+ "</a>"
				+ liner;
			}
			else if(link.equalsIgnoreCase("$admin")) {
				if(pm.userPermLevelForPage(pm.getUserName(req),currentPage)<PermissionManager.ADMIN) {
					if(tentative) {
						return "";
					}
					sayNoIcon="<img src='images/stop.png' border='0' />";
				}
				if(linkIsText) {
					text="Administer Page";
				}
				if(textIsImage) {
					text = "<img src='"+text+"' border='0' />";
				}
				return "<a href='http:adminPage.do?name="
					+ currentPage
					+ "' "
					+ popper
					+ ">"
					+ text
					+ sayNoIcon
					+ "</a>"
				+ liner;
			}
			else if(link.equalsIgnoreCase("$delete")) {
				if(!pm.canDelete(pm.getUserName(req),currentPage)) {
					if(tentative) {
						return "";
					}
					sayNoIcon="<img src='images/stop.png' border='0' />";
				}
				if(linkIsText) {
					text="Delete Page";
				}
				if(textIsImage) {
					text = "<img src='"+text+"' border='0' />";
				}
				return "<a href='http:deletePage.do?name="
					+ currentPage
					+ "' "
					+ popper
					+ ">"
					+ text
					+ sayNoIcon
					+ "</a>"
				+ liner;
			}
			else if(link.equalsIgnoreCase("$trail")) {
				String retval = "";
				String sep = "";
				Vector v = (Vector)session.getAttribute("pageTrail");
				if(v==null) return "";
				Iterator it = v.iterator();
				String uname=pm.getUserName(req);
				while(it.hasNext()) {
					String pname = (String)it.next();
					if(   PageManager.solo().pageExists(pname)
					   && pm.userPermLevelForPage(uname,pname)>=PermissionManager.READ) {
						if(pname.equals(currentPage)) {
							retval += sep + "<b>"+pname+"</b>";
						}
						else {
							retval += sep
								+ "<a href='viewPage.do?name="
													+ pname
													+ "'"
													+ popper
													+ ">"
													+ pname
													+ "</a>";
						}
						sep = "&gt; ";
					}
				}
				retval += liner;
				return retval;
			}
			else if(link.equalsIgnoreCase("$attach")){
				// if we don't have permission or current page doesn't allow attachments.
				if(currentPage.length()==0
				   || pm.userPermLevelForPage(pm.getUserName(req),currentPage)<PermissionManager.ATTACH
				   || pm.maxStoragePerAttachFor(currentPage)==0) {
					if(tentative) {
						return "";
					}
					sayNoIcon="<img src='images/stop.png' border='0' />";
				}
				if(linkIsText) {
					text="Attach file";
				}
				if(textIsImage) {
					text = "<img src='"+text+"' border='0' />";
				}
				return "<a href='http:attach.do?name="
					+ currentPage
					+ "'"
					+ popper
					+ " >"
					+ text
					+ sayNoIcon
					+ "</a>"
				+ liner;
			}
			else if(link.toLowerCase().startsWith("$showvar=")) {
				String varn = link.substring(9).trim();
				String showval = (String)vars.get(varn);
				if(showval==null) return ".null.";
				return showval;
			}
			else if(link.equalsIgnoreCase("$page")) {
				return currentPage;
			}
			else if(link.equalsIgnoreCase("$user")) {
				String userName = pm.getUserName(req);
				String guestName = pm.getGuestName(req);
				link="UserProfiles."+userName;
				if(guestName.length()>0) {
					text = userName+" ("+guestName+")";
				}
				else {
					text = userName;
				}
			}
			else if(link.equalsIgnoreCase("$children")) {
				// if the link is tentative and the user doesn't have at least EXTEND permission
				// omit the list of child pages.
				if(pm.userPermLevelForPage(pm.getUserName(req),currentPage)<PermissionManager.EXTEND) {
					if(tentative) {
						return "";
					}
				}
				// get the children.
				Vector children = PageManager.solo().listChildren(currentPage,false);
				// build the list of links to the children
				Iterator it = children.iterator();
				String retval = "<b>Child pages:</b><br />&nbsp;&nbsp;&nbsp;";
				int childCount = 0;
				String sep = "";
				while(it.hasNext()) {
					String child = (String)it.next();
					//only list children the user can at least read
					if(pm.userPermLevelForPage(pm.getUserName(req),child)>=PermissionManager.READ) {
						childCount++;
						retval += sep + "<a href='viewPage.do?name="+child+"'>"+child+"</a>";
						sep = ", ";
					}
				}
				if(childCount==0) {
					return "";
				}
				return retval;
			}
			else if(link.equalsIgnoreCase("$attachments")) {
			
				// get the attachments.
				Vector attachments = PageManager.solo().listAttachments(currentPage);
				
				// If there are none, just return.
				if(attachments.size()==0) {
					return "";
				}

				// build the list of links to the Attachments
				Iterator it = attachments.iterator();
				String retval = "<b>Attachments:</b><br />&nbsp;&nbsp;&nbsp;";
				String sep = "";
				while(it.hasNext()) {
					String attachment = (String)it.next();			
					String fullName = currentPage+"/"+attachment;
					retval += sep;
					retval += "<a href='viewAttachment.do?name="+fullName+"'>"+attachment+"</a>";
					retval += " (" + PageInfoNode.lookUpAttSize(currentPage,attachment) + " bytes)";
					if(pm.userPermLevelForPage(pm.getUserName(req),currentPage)>=PermissionManager.ATTACH) {
						retval += "&nbsp;&nbsp;[<a href='deletePage.do?name="+fullName+"&targetIsAttachment=YES'>"
						          +"<img src='http:images/delete.png' border='0' align='middle'/>Delete"
						          +"</a>&nbsp;]";
					}
					sep = "<br clear='all' />&nbsp;&nbsp;&nbsp;";
				}
				
				return retval;
			}
			else {
				return link+"?";
			}
		}

		// non-wiki links will contain at least one colon (as in http:)
		if (link.indexOf(':') >= 0) {

			// If we are inlining the link try to figure out what to do based on the extension.
			if (inline) {
				int dot = link.lastIndexOf('.');
				if (dot >= 0) {
					String ext = link.substring(dot + 1).trim().toLowerCase();
					if (extIsImage(ext)) {
						return "<img src='"
							+ link
							+ "' alt='"
							+ text
							+ "'"
							+ sider
							+ popper
							+ ">"
							+ liner;
					}
					// TODO Look into better ways to inline & other things we can inline (.txt for instance).
				}
			}
			if(textIsImage) {
				text = "<img src='"+text+"' border='0'"+sider+" />";
			}
			return "<a href='"
				+ link
				+ "'"
				+ popper
				+ ">"
				+ text
				+ "&nbsp;<img src='images/out.png' border='0' "
					+ popper
					+ "></a>"
				+ liner;
		}
		// The presumption at this point is that we have a wiki page link
		else {
			// if the link is just a dot it is a reference to the "current" page.
			// (used by templates to "inline" the current page.
			if(link.trim().equals(".")) {
				link=currentPage;
			}
			// Similarly, if the link is two dots it refers to the wiki base.
			if(link.trim().equals("..")) {
				link="";
			}
			
			// Various translations to make a 'legal' page name.
			link = crushName(link);

			// is the page an attachment?
			boolean isAttachment = link.indexOf('/') >= 0;
			
			// decide if page is found and what permission the user has to access it.
			boolean pageFound = 
				isAttachment? 
					AttachmentManager.solo().attachmentExists(link) 
				:	PageManager.solo().pageExists(link);
			
			// decide if the parent is present.  Names that start with a dot cannot find their parent
			// by this method so assume false.
			boolean parentFound = !link.startsWith(".") 
			                    && PageManager.solo().pageExists(pm.parentOf(link));
			
				
			int userPerm = pm.userPermLevelForPage(user, link);
			
			// special cases for examples.
			if(link.startsWith("Null")) {
				if(link.equals("NullPageOne")) {
					userPerm = PermissionManager.EXTEND;
					pageFound = false;
				}
				else if(link.equals("NullPageTwo")) {
					userPerm = PermissionManager.NO_ACCESS;
					pageFound = true;
				}
			}

			// if the page is there and the user can at least READ it, we are happy.
			if (pageFound && userPerm >= PermissionManager.READ) {
				// Handle a link to an existing wiki attachment.
				if (isAttachment) {
					if (inline) {
						int dot = link.lastIndexOf('.');
						if (dot >= 0) {
							String ext = link.substring(dot + 1);
							if (extIsRawText(ext)) {
								try {
									return "<pre>\n"
										+ AttachmentManager.solo().getAttachment(link)
										+ "</pre>\n";
								} catch (PageManagerException e) {
									// TODO Auto-generated catch block
									e.printStackTrace();
								}
							}
							if (extIsImage(ext)) {
								return "<img src='viewAttachment.do?name="
									+ link
									+ "' alt='"
									+ text
									+ "'"
									+ sider
									+ popper
									+ ">"
									+ liner;
							}
						}
						// TODO What other attachments can we inline?
					}
					if(textIsImage) {
						text = "<img src='"+text+"' border='0' "+sider+"/>";
					}
					return "<a href='viewAttachment.do?name="
						+ link
						+ "'"
						+ popper
						+ ">"
						+ text
						+ "</a>"
						+ liner;
				}
				// Handle a link to an existing wiki page (not an attachment)
				else {
					// if we are going to inline the page...
					if (inline) {
						HashMap ancestors =
							(HashMap) req.getAttribute("ancestors");
						if (ancestors == null) {
							ancestors = new HashMap();
							req.setAttribute("ancestors",ancestors);
						}
						if (ancestors.containsKey(link)) {
							return "<font color='red'>Recursive Inline Link</font> to "
								+ link
								+ "\n";
						}
						ancestors.put(link, link);
						try {
							String retval = (new StikiWebTranslator()).translateText(
								PageManager.solo().getPage(link), 
								currentPage,
								link,
								req);
							ancestors.remove(link);
							return retval;
						} catch (Exception e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
						ancestors.remove(link);
					}
					if(textIsImage) {
						text = "<img src='"+text+"' border='0' "+sider+"/>";
					}
					if(linkparms!=null) {
						Iterator it = linkparms.entrySet().iterator();
						while(it.hasNext()) {
							Map.Entry entry = (Map.Entry)it.next();
							link += "&var_"+entry.getKey()+"="+entry.getValue();
						}
					}
					return "<a href='viewPage.do?name="
						+ link
						+ "'"
						+ popper
						+ ">"
						+ text
						+ "</a>"
						+ liner;
				}
			}
			// otherwise, if the user can create the page and it is not there then also happy.
			// Omit attachments since user can't create them just by editing the page.
			// Don't allow user to create children of non-existent pages (parentFound but not pageFound).
			else if (!isAttachment && userPerm >= PermissionManager.EXTEND && !pageFound && parentFound) {
				if(textIsImage) {
					text = "<img src='"+text+"' border='0' "+sider+"/>";
				}
				if(linkparms!=null) {
					Iterator it = linkparms.entrySet().iterator();
					while(it.hasNext()) {
						Map.Entry entry = (Map.Entry)it.next();
						link += "&var_"+entry.getKey()+"="+entry.getValue();
					}
				}
				return "<a href='createPage.do?name="
					+ link
					+ "'"
					+ popper
					+ ">"
					+ text
					+ "&nbsp;<img src='images/add.png' border='0' /></a>"
					+ liner;
			}
			// Either the page exists and user can't view it or it does not exist and user can't create it.
			// Unless it is tentative, create a link to the page not found screen.
			else if (!tentative) {
				if(textIsImage) {
					text = "<img src='"+text+"' border='0' "+sider+"/>";
				}
				return "<a href='noPage.jsp?name="
					+ link
					+ "'"
					+ popper
					+ ">"
					+ text
					+ "<img src='images/stop.png' border='0' /></a>"
					+ liner;
			}
		}
		// This should be a tentative link to an inaccessible page -- return no link at all.
		return "";
	}
	
	public HashMap parseLinkParms(String link) {
		// link is of the form link ( this = that, other = somethingElse, ... )
		int parenloc = link.indexOf('(');
		// if there is no paren there are no parms
		if(parenloc<0) return null;
		String body = link.substring(parenloc+1).trim();
		if(body.endsWith(")")) {
			body = body.substring(0,body.length()-1);
		}
		// body is now part inside the parens (or to the end if paren not closed)
		HashMap retval = new HashMap();
		StringTokenizer tok = new StringTokenizer(body,",");
		// pull off segments separated by commas
		while(tok.hasMoreTokens()) {
			String segment = tok.nextToken().trim();
			int equloc = segment.indexOf('=');
			String var = segment;
			String val = "true";
			if(equloc>=0) {
				var=segment.substring(0,equloc).trim();
				val=segment.substring(equloc+1).trim();
			}
			try {
				var = URLEncoder.encode(var,"UTF-8");
				val = URLEncoder.encode(val,"UTF-8");
			} catch (UnsupportedEncodingException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			// value can be empty but variable name cannot
			if(var.length()>0) retval.put(var,val);
		}
		// if we found at least one parm return the map.
		return (retval.size()==0) ? null : retval;
	}

	public boolean extIsRawText(String ext) {
		return ("txt".equals(ext) || "asc".equals(ext));
	}

	public boolean extIsImage(String ext) {
		return (
			"gif".equals(ext)
				|| "jpg".equals(ext)
				|| "bmp".equals(ext)
				|| "png".equals(ext)
				|| "jpeg".equals(ext));
	}

	/**
	 * Converts a string from the markup language to a Wiki Page name.  
	 * Spaces are removed and the first character of each word is capitalized.
	 * If the string contains a colon it is treated as a URL and these 
	 * transformations are not applied.  If it is not a URL and it contains
	 * a slash character, anything after the slash is treated as an attachment
	 * name and also not translated.
	 *
	 * Note that two translations are applied to a markup string for a page
	 * to get to the actual file name.  First, this routine is called to create
	 * the internal wiki page name (that resembles a java package name).  
	 * The second step (see convertName below) translates that to a file name
	 * and path for the actual disk file reference.
	 * 
	 * @param in - String in markup that represents a page
	 * @return - String containing canonical wiki page name.
	 */
	public static String crushName(String in) {
		if (in.indexOf(':') > 0) {
			// assume we have a hard-coded URL (such as http://google.com).
			// Double any quotes it contains and return it.
			return fixQuotes(in);
		} else {
			// link is either a wiki page name or a wiki page attachment name.
			// crush out spaces and stray punctuation up to the optional slash
			// that marks the beginning of the attachment name.  Capitalize the
			// first characters of any words.
			StringBuffer sb = new StringBuffer();
			int len = in.length();
			boolean foundSlash = false;
			boolean forceCaps = true;
			for (int i = 0; i < len; i++) {
				char c = in.charAt(i);
				if (Character.isJavaIdentifierPart(c)) {
					if (forceCaps && !foundSlash) {
						// capitalize the first character of words but not in the
						// attachment file name (that follows the slash).
						sb.append(Character.toUpperCase(c));
						forceCaps = false;
					} else {
						sb.append(c);
					}
				} else if (Character.isWhitespace(c)) {
					if (foundSlash) {
						// preserve spaces in attachment file name part
						sb.append(' ');
					}
					forceCaps = true;
				} else if (c == '.') {
					// leave periods in but force capitalization of following word
					sb.append(c);
					forceCaps = true;
				} else if (c == '/') {
					// slash separates page name from attachment file name.
					sb.append(c);
					foundSlash = true;
				}
			}
			return sb.toString();
		}
	}

	public static String fixQuotes(String in) {
		StringBuffer sb = new StringBuffer();
		int len = in.length();
		for (int i = 0; i < len; i++) {
			char c = in.charAt(i);
			if (c == '\'') {
				sb.append('\'');
			}
			sb.append(c);
		}
		return sb.toString();
	}

	/**
	 * Converts a name in the general form [[ancestor.]*parent.]page[/attachment]
	 * to its file path under WikiBase.  The new string will be in the general form -
	 * [[Ancestor/]*Parent/]Page[/attachment | .wiki ]. 
	 * 
	 * Or to put it another way, wiki pages are generally structured like java packages
	 * where the name is a series of identifiers separated by dots to represent the 
	 * nesting of the directories.  (Note: I am referring to the typical implementation 
	 * of Java packages as nested directories -- the definition of packages allows for
	 * other storage schemes.)  The dots separating the nesting levels are translated 
	 * to slashes to form a file path.  Punctuation (except as noted here) is discarded. 
	 * 
	 *  The page name can be followed by a slash  and the name of a page attachment.  
	 * Attachments names are the page name followed by a dot and the name of the 
	 * attachment.
	 * 
	 * The conversion will ensure that all words in the Wiki page name are capitalized
	 * and will remove all spaces from the page name.  The capitalization of the 
	 * attachment name is not changed.
	 * 
	 * Examples:
	 * "main page/ hello.html"  >>  "MainPage/hello.html"
	 * "main . Page 123.footnotes" >> Main/Page123/Footnotes"
	 * "MY(cool)page.stuff" >> "MyCoolPage.Stuff"
	 * foo. bar. moo / biff.pow.zip" >> "Foo/Bar/Moo/biff.pow.zip"
	 * 
	 * The last example above can be read as "an attachment named biff.pow.zip attached
	 *  to a wiki page named Moo which, in turn is a child of a page named Bar which
	 * is a child of a page named Foo."
	 * @param in - String containing the name to convert
	 * @return - the converted name as a file path string.
	 */
	public static String convertName(String in) {
		StringBuffer sb = new StringBuffer();
		int len = in.length();
		boolean slashFound = false;
		boolean forceCap = true;
		for (int i = 0; i < len; i++) {
			char c = in.charAt(i);
			if (Character.isJavaIdentifierPart(c)) {
				//Force capitalization of first char in a word in Wiki page names
				//but not in attachment names.
				if (forceCap && !slashFound) {
					sb.append(Character.toUpperCase(c));
					forceCap = false;
				} else {
					sb.append(c);
				}
			} else if (c == '/') {
				// this allows page attachments to be directories.
				sb.append('/');
				slashFound = true;
			} else if (c == '.') {
				//dots in Wiki page names indicate child directory (similar to Java packages)
				//but after the slash (in the attachment name part) they are not translated.
				if (slashFound) {
					sb.append('.');
				} else {
					sb.append('/');
				}
				forceCap = true;
			} else if (c == ' ') {
				//spaces in the Wiki name separate words, forcing caps but taken out.
				if (slashFound) {
					sb.append(' ');
				} else {
					forceCap = true;
				}
			} else {
				forceCap = true;
			}
		}
		if (!slashFound) {
			sb.append(".wiki");
		}
		return sb.toString();
	}

	protected String sanitizeParms(String in) {
		// TODO write code to sanitize tag attributes
		return in;
	}

	protected String sanitizeText(String in) {
		StringBuffer sb = new StringBuffer();
		int len = in.length();
		for (int i = 0; i < len; i++) {
			char c = in.charAt(i);
			switch (c) {
				case '<' :
					sb.append("&lt;");
					break;
				case '&' :
					sb.append("&amp;");
					break;
				default :
					sb.append(c);
			}
		}
		return sb.toString();
	}

	protected int findUnescaped(char c, String s, int start) {
		boolean escaped = false;
		int len = s.length();
		for (int i = start; i < len; i++) {
			char sc = s.charAt(i);
			if (sc == '\\') {
				escaped = !escaped;
			} else if (sc == c && !escaped) {
				return i;
			} else {
				escaped = false;
			}
		}
		return -1;
	}
	
	public static String capitalize(String in) {
		if(in==null) return in;
		if(in.length()<2) return in.toUpperCase();
		return in.substring(0,1).toUpperCase() + in.substring(1);
	}
}
